#include "gl.hh"
#include "camera.hh"
#include "ease-out.hh"
#include <utility>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <glm/mat3x3.hpp>
#include <glm/trigonometric.hpp>
#include <glm/gtx/rotate_vector.hpp>
#include <glm/geometric.hpp>

//
// tex[0]{0,3,6}: has mipmap
// tex[1]       :  no mipmap
// tex[2]       :  no mipmap
// tex[3]       : has mipmap, for exposure control
//
// stage[0]: scene render
//   target: tex[2]: the scene image
//
// stage[1]: fxaa
//    input: tex[2]: scene
//   target: tex[1]: anti-aliased scene
//
// stage[2]: brightpass
//    input: tex[1]: anti-aliased
//   target: tex[0]: brightpass
//
// stage[3]: light scatter
//    input: tex[0]: brightpass
//   target: tex[2]: scattered brightpass
//
// stage[4]: gaussian blur
//    input: tex[0]{3,6}: brightpass
//   target: tex[0]{ 0 }: gaussian blur (3+6 mipmap) of the brightpass
//
// stage[5]: composite
//    input: tex[0]: blur
//    input: tex[1]: anti-aliased scene
//    input: tex[2]: scatter
//   target: tex[3]: final render, tex[0]+tex[1]+tex[2] for bloom & scatter effect
//
// stage[6]: display
//    input: tex[3]: final render
//   target: screen: final render
//
namespace
{
    GLuint tex[4];
    GLuint fbo[4];

    GLuint program_scene;
    GLuint program_fxaa;
    GLuint program_brightpass;
    GLuint program_scatter;
    GLuint program_gaussian;
    GLuint program_composite;
    GLuint program_display;

    GLint scene_time;
    GLint scene_pos;
    GLint scene_player;
    GLint scene_axis;
    GLint scene_focal;
    GLint scene_sun_dir;
    GLint scene_player_alpha;
    GLint scene_texture_filtering;
    GLint scatter_center;
    GLint scatter_amount;
    GLint gaussian_dir;
    GLint gaussian_time;
    GLint composite_scene;
    GLint composite_bloom;
    GLint composite_scatter;
    GLint composite_exposure;
    GLint display_alpha;

    constexpr auto default_focal = 1.5f;
    constexpr auto default_zoom_focal = 3.0f;
    const auto default_sun_rot = glm::vec2{0.4, 0};
    constexpr auto day_brightness = 0.30f;
    constexpr auto night_brightness = 0.15f;
    gl::camera cam;
    gl::ease_out<float> cam_focal{default_focal};
    float zoom_focal{default_zoom_focal};
    float target_brightness = day_brightness;
    float cam_exposure{};
    float target_exposure{};
    gl::ease_out<glm::vec2> sun_rot{default_sun_rot};
    gl::ease_out<float> scene_alpha{1, 0};
    gl::ease_out<float> bloom_alpha{1, 0};
    gl::ease_out<float> scatter_alpha{1, 0};
    gl::ease_out<float> player_alpha{1, 0};
    gl::ease_out<float> output_alpha{0.1f, 1};
    gl::ease_out<float> texture_filtering{1, 0};

    glm::vec2 fb_size;

    glm::vec3 views[] = {
        {  0.0, 0.0,  1.0 },
        { -0.1, 0.1,  1.0 },
        {  0.0, 0.1,  1.0 },
        { +0.1, 0.1,  1.0 },
        {  0.0, 0.0, -0.1 },
    };
    int current_view = 0;
}


//------ shader and program
namespace detail
{
    static void attach_shader(GLuint) {}

    template <class ...Ts>
    static void attach_shader(GLuint p, GLuint s, Ts... ss)
    {
        glAttachShader(p, s);
        attach_shader(p, ss...);
    }
}

static auto create_shader(GLenum type, char const* source)
{
    auto s = glCreateShader(type);
    glShaderSource(s, 1, &source, {});
    glCompileShader(s);
    return s;
}


template <class ...Ts>
static auto create_program(GLuint s, Ts... shaders)
{
    auto p = glCreateProgram();
    detail::attach_shader(p, s, shaders...);
    glLinkProgram(p);
    return p;
}

static auto create_program(char const* vsrc, char const* fsrc)
{
    auto vs = create_shader(GL_VERTEX_SHADER  , vsrc);
    auto fs = create_shader(GL_FRAGMENT_SHADER, fsrc);
    return create_program(vs, fs);
}


//------ texture
#ifndef NDEBUG
static void texture_resize(int w, int h);
#endif
namespace detail
{
    static void create_texture(int idx)
    {
        glActiveTexture(GL_TEXTURE0 + idx);
        glGenTextures(1, &tex[idx]);
        glBindTexture(GL_TEXTURE_2D, tex[idx]);
#ifndef NDEBUG
        texture_resize(0, 0);   // will re-allocate when resizing
#endif
        //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        // no GL_TEXTURE_MIN_FILTER intensionally
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    }
}

static void create_texture(int idx) // no mipmap
{
    detail::create_texture(idx);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
}

static void create_texture(int idx, int max_level)  // with mipmap
{
    detail::create_texture(idx);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, max_level);
}

static void texture_resize(int w, int h)
{
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, {});
}


//------ framebuffer
namespace detail
{
    static void create_framebuffer(int idx)
    {
        glGenFramebuffers(1, &fbo[idx]);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo[idx]);
    }

    // framebuffer texture
    void framebuffer_texture(int) {}

    template <class ...Ts>
    static void framebuffer_texture(int attachment_idx, int tex_idx, Ts... tex_idxs)
    {
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0+attachment_idx, GL_TEXTURE_2D, tex[tex_idx], 0);
        framebuffer_texture(attachment_idx+1, tex_idxs...);
    }

    // framebuffer drawbuffers
    template <int N, int ...Is>
    static void framebuffer_drawbuffers(std::integer_sequence<int, Is...>)
    {
        GLenum buffers[N] = { GL_COLOR_ATTACHMENT0 + Is... };
        glDrawBuffers(N, buffers);
    }

    template <int N>
    static void framebuffer_drawbuffers()
    {
        framebuffer_drawbuffers<N>(std::make_integer_sequence<int, N>{});
    }
}

static void create_framebuffer(int idx, int tex_idx)
{
    detail::create_framebuffer(idx);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex[tex_idx], 0);
}

template <class ...Ts>
static void create_framebuffer(int idx, Ts... tex_idxs)
{
    detail::create_framebuffer(idx);
    detail::framebuffer_texture(0, tex_idxs...);
    detail::framebuffer_drawbuffers<sizeof...(tex_idxs)>();
}

void update(float time)
{
    // update scene (time and camera)
    cam.update();
    cam_focal.update(0.1);
    sun_rot.update(0.1);
    player_alpha = glm::clamp(glm::abs(cam.eye_dist()) - 0.08f, 0.0f, 0.1f) / 0.1f;
    player_alpha.update(0.1);
    output_alpha.update(0.1);
    texture_filtering.update(0.1);
    auto& axis = cam.get();
    auto sun_dir = rotateY(rotateX(glm::vec3{0, 0, -1}, sun_rot.get()[0]), sun_rot.get()[1]);
    target_brightness = glm::mix(night_brightness, day_brightness, (glm::clamp(sun_dir.y, -0.3f, 0.2f) + 0.3f) / 0.5f);

    glUseProgram(program_scene);
    glUniform1f(scene_time, time);
    glUniform3fv(scene_pos, 1, &cam.eye()[0]);
    glUniform3fv(scene_player, 1, &cam.pos()[0]);
    glUniformMatrix3fv(scene_axis, 1, false, &axis[0][0]);
    glUniform1f(scene_focal, glm::pow(2, cam_focal));
    glUniform3fv(scene_sun_dir, 1, &sun_dir[0]);
    glUniform1f(scene_player_alpha, player_alpha);
    glUniform1f(scene_texture_filtering, texture_filtering);

    // update gaussian blur (time)
    glUseProgram(program_gaussian);
    glUniform1f(gaussian_time, time);

    // update scatter (scatter center and amount)
    auto x = dot(sun_dir, axis[0]) + 0.5;
    auto y = dot(sun_dir, axis[1]) + 0.5;
    auto amount = dot(sun_dir, -axis[2]);

    glUseProgram(program_scatter);
    glUniform2f(scatter_center, x, y);
    glUniform1f(scatter_amount, amount);

    // update composite (exposure)
    cam_exposure = glm::mix(cam_exposure, target_exposure,
            (target_exposure > cam_exposure ? 0.1 : 0.02));
    scene_alpha.update(0.1);
    bloom_alpha.update(0.1);
    scatter_alpha.update(0.1);

    glUseProgram(program_composite);
    glUniform1f(composite_exposure, cam_exposure);
    glUniform1f(composite_scene, scene_alpha);
    glUniform1f(composite_bloom, bloom_alpha);
    glUniform1f(composite_scatter, scatter_alpha);

    glUseProgram(program_display);
    glUniform1f(display_alpha, output_alpha);
}

void post_update()
{
    constexpr auto nsample = 3;
    glm::vec4 major_sample{};
    for (auto ix=0; ix<nsample; ix++)
        for (auto iy=0; iy<nsample; iy++) {
            auto x = float(ix) / (nsample+1);
            auto y = float(iy) / (nsample+1);
            glm::vec4 color;
            glReadPixels(x*fb_size.x, y*fb_size.y, 1, 1, GL_RGBA, GL_FLOAT, &color[0]);
            major_sample += color;
        }
    major_sample /= nsample * nsample;

    glm::vec4 minor_sample;
    int level = glm::floor(glm::log2(glm::max(fb_size.x, fb_size.y)));
    glActiveTexture(GL_TEXTURE3);
    glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_FLOAT, &minor_sample[0]);

    auto sample = mix(minor_sample, major_sample, 0.7);

    auto brightness = glm::mix(target_brightness, dot(sample, {0.30, 0.59, 0.11, 0}), scene_alpha.get());
    target_exposure -= brightness - target_brightness - (cam_exposure - target_exposure);
    fprintf(stderr, "level=%02d    brightness = %+6.3f    exp = %+6.3f    target = %+6.3f\r",
            level, brightness, cam_exposure, target_exposure);
}


auto make_edge_trigger(bool old=false)
{
    return [old](bool x, auto up, auto down) mutable {
        if (old == x) return;
        old = x;
        if (x) up();
        else down();
    };
}


int main()
{
    gl::init();

    glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);

    //------ create vao, vbo, textures, framebuffers and programs
    {
        GLuint id;
        glGenVertexArrays(1, &id);
        glBindVertexArray(id);
    }

    {
        GLbyte points[] = {     // a full-screen quad
            -1, -1,    +1, -1,    -1, +1,
            +1, +1,    -1, +1,    +1, -1,
        };
        GLuint id;
        glGenBuffers(1, &id);
        glBindBuffer(GL_ARRAY_BUFFER, id);
        glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 2, GL_BYTE, false, 0, {});
    }


    create_texture(0, 6);
    create_texture(1);
    create_texture(2);
    create_texture(3);

    create_framebuffer(0, 0);
    create_framebuffer(1, 1);
    create_framebuffer(2, 2);
    create_framebuffer(3, 3);


    {
        program_scene = create_program(
            #include "shader/scene"
        );
        scene_time = glGetUniformLocation(program_scene, "time");
        scene_pos = glGetUniformLocation(program_scene, "camera_pos");
        scene_player = glGetUniformLocation(program_scene, "player_pos");
        scene_axis = glGetUniformLocation(program_scene, "camera_axis");
        scene_focal = glGetUniformLocation(program_scene, "focal");
        scene_sun_dir = glGetUniformLocation(program_scene, "sun_dir");
        scene_player_alpha = glGetUniformLocation(program_scene, "player_alpha");
        scene_texture_filtering = glGetUniformLocation(program_scene, "texture_filtering");
    }

    {
        program_fxaa = create_program(
            #include "shader/fxaa"
        );
        glUseProgram(program_fxaa);
        glUniform1i(glGetUniformLocation(program_fxaa, "scene"), 2);    // tex[2]: scene
    }

    {
        program_brightpass = create_program(
            #include "shader/brightpass"
        );
        glUseProgram(program_brightpass);
        glUniform1i(glGetUniformLocation(program_brightpass, "scene"), 1);  // tex[1]: anti-aliased scene
    }

    {
        program_scatter = create_program(
            #include "shader/scatter"
        );
        glUseProgram(program_scatter);
        glUniform1i(glGetUniformLocation(program_scatter, "scene"), 0); // tex[0]: brightpass
        scatter_center = glGetUniformLocation(program_scatter, "center");
        scatter_amount = glGetUniformLocation(program_scatter, "amount");
    }


    {
        program_gaussian = create_program(
            #include "shader/gaussian"
        );
        glUseProgram(program_gaussian);
        glUniform1i(glGetUniformLocation(program_gaussian, "tex"), 0);  // tex[0]: brightpass
        gaussian_dir = glGetUniformLocation(program_gaussian, "dir");   // blur direction
        gaussian_time = glGetUniformLocation(program_gaussian, "time"); // blur sampling random around time
    }

    {
        program_composite = create_program(
            #include "shader/composite"
        );
        glUseProgram(program_composite);
        glUniform1i(glGetUniformLocation(program_composite, "blur"), 0);    // tex[0]: blur
        glUniform1i(glGetUniformLocation(program_composite, "scene"), 1);   // tex[1]: scene
        glUniform1i(glGetUniformLocation(program_composite, "scatter"), 2); // tex[1]: scatter
        composite_exposure = glGetUniformLocation(program_composite, "exposure");
        composite_scene = glGetUniformLocation(program_composite, "scene_alpha");
        composite_bloom = glGetUniformLocation(program_composite, "bloom_alpha");
        composite_scatter = glGetUniformLocation(program_composite, "scatter_alpha");
    }

    {
        program_display = create_program(
            #include "shader/display"
        );
        glUseProgram(program_display);
        glUniform1i(glGetUniformLocation(program_display, "scene"), 3); // tex[3]: final render
        display_alpha = glGetUniformLocation(program_display, "alpha");
    }




    // fix trigger default value
    gl::axis("joy.2", -1);
    gl::axis("joy.5", -1);

    auto change_view_trigger = make_edge_trigger();
    auto reset_trigger = make_edge_trigger();
    auto zoom_trigger = make_edge_trigger();
    auto scene_toggle_trigger = make_edge_trigger();
    auto bloom_toggle_trigger = make_edge_trigger();
    auto scatter_toggle_trigger = make_edge_trigger();
    auto texture_filtering_toggle_trigger = make_edge_trigger();

    auto forget_TAA_history = [&] {
        output_alpha = 1.0f;
        output_alpha.update(1.0f);
        output_alpha = 0.05f;
    };

    auto render = [&](float time) {
        //------ update
        constexpr float s = 0.03;
        // pos
        if (gl::button("w")) { cam.zmove(-s); forget_TAA_history(); }
        if (gl::button("s")) { cam.zmove(+s); forget_TAA_history(); }
        if (gl::button("a")) { cam.xmove(-s); forget_TAA_history(); }
        if (gl::button("d")) { cam.xmove(+s); forget_TAA_history(); }
        if (gl::button("q")) { cam.ymove(-s); forget_TAA_history(); }
        if (gl::button("e")) { cam.ymove(+s); forget_TAA_history(); }
        cam.xmove(gl::axis("joy.0") * s * 2);
        cam.zmove(gl::axis("joy.1") * s * 2);
        cam.ymove((gl::axis("joy.5") - gl::axis("joy.2")) * s);
        // target
        if (gl::button("i")) { cam.rotate({+s, 0, 0}); forget_TAA_history(); }
        if (gl::button("k")) { cam.rotate({-s, 0, 0}); forget_TAA_history(); }
        if (gl::button("j")) { cam.rotate({0, +s, 0}); forget_TAA_history(); }
        if (gl::button("l")) { cam.rotate({0, -s, 0}); forget_TAA_history(); }
        cam.rotate({0, -gl::axis("joy.3") * s * 2, 0});
        cam.rotate({-gl::axis("joy.4") * s * 2, 0, 0});
        // focal
        if (gl::button("o")) { cam_focal -= s; forget_TAA_history(); }
        if (gl::button("u")) { cam_focal += s; forget_TAA_history(); }
        if (gl::button("joy.0")) cam_focal = zoom_focal += (gl::button("joy.5") - gl::button("joy.4")) * s;
        zoom_trigger(gl::button("joy.0"),
                []{},
                []{ cam_focal = default_focal; });
        // sun rotation y
        if (gl::button("h")) sun_rot.target()[1] -= s;
        if (gl::button("f")) sun_rot.target()[1] += s;
        if (gl::button("g")) sun_rot.target()[0] -= s;
        if (gl::button("t")) sun_rot.target()[0] += s;
        sun_rot.target()[0] += gl::axis("joy.hat.0.y") * s;
        sun_rot.target()[1] -= gl::axis("joy.hat.0.x") * s;
        // change view
        change_view_trigger(gl::button("tab") || gl::button("joy.10"), []{
            current_view++;
            current_view %= sizeof(views) / sizeof(views[0]);
            cam.dist(views[current_view]);
        }, []{});
        // reset
        reset_trigger(gl::button("`") || gl::button("joy.9"), []{
            cam.reset();
            current_view = 0;
            zoom_focal = default_zoom_focal;
            cam_focal = default_focal;
            sun_rot = default_sun_rot;
        }, []{});
        // quit
        if (gl::button("escape") || gl::button("joy.7"))
            gl::quit();
        // effect toggles
        scene_toggle_trigger(gl::button("1"), []{ scene_alpha.flip(); }, []{});
        bloom_toggle_trigger(gl::button("2"), []{ bloom_alpha.flip(); }, []{});
        scatter_toggle_trigger(gl::button("3"), []{ scatter_alpha.flip(); }, []{});
        texture_filtering_toggle_trigger(gl::button("4"), []{ texture_filtering.flip(); }, []{});
        // apply the update
        update(time);


        //------ render scene to tex[0]
        glUseProgram(program_scene);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo[2]);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        //------ fxaa from tex[2] to tex[1]
        glUseProgram(program_fxaa);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        //------ brightpass from tex[1] to tex[0]
        glUseProgram(program_brightpass);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glActiveTexture(GL_TEXTURE0);
        glGenerateMipmap(GL_TEXTURE_2D);

        //------ scatter from tex[0] to tex[2]
        glUseProgram(program_scatter);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo[2]);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        //------ render gaussian blur from tex[0]{1} to tex[0]{0}
        glUseProgram(program_gaussian);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]);

        // along x axis
        glUniform2f(gaussian_dir, 1, 0);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        // assume: glActiveTexture(GL_TEXTURE0);
        glGenerateMipmap(GL_TEXTURE_2D);

        // along y axis
        glUniform2f(gaussian_dir, 0, 1);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        // assume: glActiveTexture(GL_TEXTURE0);
        glGenerateMipmap(GL_TEXTURE_2D);

        //------ composite tex[0]+tex[1]+tex[2] to tex[3]
        glUseProgram(program_composite);
        glBindFramebuffer(GL_FRAMEBUFFER, fbo[3]);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glActiveTexture(GL_TEXTURE3);
        glGenerateMipmap(GL_TEXTURE_2D);

        //------ display tex[3] to screen
        glEnable(GL_BLEND);
        glUseProgram(program_display);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glDrawArrays(GL_TRIANGLES, 0, 6);
        glDisable(GL_BLEND);


        //------ update based on rendered scene
        post_update();
    };

    auto resize = [&](int w, int h) {
        fb_size = {w, h};
        glViewport(0, 0, w, h);

        glActiveTexture(GL_TEXTURE0);
        texture_resize(w, h);

        glActiveTexture(GL_TEXTURE1);
        texture_resize(w, h);

        glActiveTexture(GL_TEXTURE2);
        texture_resize(w, h);

        glActiveTexture(GL_TEXTURE3);
        texture_resize(w, h);
    };

    gl::run(std::move(render), std::move(resize));
}

